CVE-2026-5223: prohibit unpacking symlinks and other unexpected entries
authorJosh Triplett <josh@joshtriplett.org>
Mon, 30 Mar 2026 17:35:55 +0000 (10:35 -0700)
committerFabian Grünbichler <debian@fabian.gruenbichler.email>
Fri, 24 Jul 2026 16:02:33 +0000 (18:02 +0200)
Cargo has historically not allowed creating .crate packages containing
symlinks. (It packages the symlink target in place of the symlink,
instead.) So, any package containing a symlink would have to be
hand-constructed. Such packages are also not allowed on crates.io, so it
could only come from an alternate registry.

Rather than dealing with symlink traversal attacks when unpacking a
crate, just prohibit symlinks entirely.

In the process, also prohibit other kinds of unusual entries. As an
exception, allow character devices but warn about them, because some
exist in crates on crates.io.

FG: backported from 1.96.0
Signed-off-by: Fabian Grünbichler <git@fabian.gruenbichler.email>
Gbp-Pq: Topic cargo
Gbp-Pq: Name CVE-2026-5223-prohibit-unpacking-symlinks-and-other-unexp.patch

src/tools/cargo/src/cargo/sources/registry/mod.rs
src/tools/cargo/tests/testsuite/registry.rs

index bf10f81fc201e95f3bcfecb06c274ddb0f6c6d8a..313258fc9d86b28973c15f5882731e08a6a51f16 100644 (file)
@@ -197,7 +197,7 @@ use cargo_util::paths::{self, exclude_from_backups_and_indexing};
 use flate2::read::GzDecoder;
 use serde::Deserialize;
 use serde::Serialize;
-use tar::Archive;
+use tar::{Archive, EntryType};
 use tracing::debug;
 
 use crate::core::dependency::Dependency;
@@ -662,6 +662,15 @@ impl<'gctx> RegistrySource<'gctx> {
                     prefix
                 )
             }
+
+            // Prevent unpacking symlinks and other unexpected entry types
+            match entry.header().entry_type() {
+                EntryType::Regular | EntryType::Directory => {}
+                t => anyhow::bail!(
+                    "invalid tarball downloaded, contains an entry at {entry_path:?} with invalid type {t:?}",
+                ),
+            }
+
             // Prevent unpacking the lockfile from the crate itself.
             if entry_path
                 .file_name()
index 8498713701c498bb3fee5c90bf9b05a61dfd8805..df922efbdf954d8dfdc1a50897e85638ed8d58b0 100644 (file)
@@ -3213,8 +3213,7 @@ fn package_lock_inside_package_is_overwritten() {
 }
 
 #[cargo_test]
-fn package_lock_as_a_symlink_inside_package_is_overwritten() {
-    let registry = registry::init();
+fn package_lock_as_a_symlink_inside_package_is_invalid() {
     let p = project()
         .file(
             "Cargo.toml",
@@ -3237,21 +3236,23 @@ fn package_lock_as_a_symlink_inside_package_is_overwritten() {
         .symlink(".cargo-ok", "src/lib.rs")
         .publish();
 
-    p.cargo("check").run();
+    p.cargo("check")
+        .with_status(101)
+        .with_stderr_data(str![[r#"
+[UPDATING] `dummy-registry` index
+[LOCKING] 1 package to latest compatible version
+[DOWNLOADING] crates ...
+[DOWNLOADED] bar v0.0.1 (registry `dummy-registry`)
+[ERROR] failed to download replaced source registry `crates-io`
 
-    let id = SourceId::for_registry(registry.index_url()).unwrap();
-    let hash = cargo::util::hex::short_hash(&id);
-    let pkg_root = paths::cargo_home()
-        .join("registry")
-        .join("src")
-        .join(format!("-{}", hash))
-        .join("bar-0.0.1");
-    let ok = pkg_root.join(".cargo-ok");
-    let librs = pkg_root.join("src/lib.rs");
+Caused by:
+  failed to unpack package `bar v0.0.1 (registry `dummy-registry`)`
 
-    // Is correctly overwritten and doesn't affect the file linked to
-    assert_eq!(ok.metadata().unwrap().len(), 7);
-    assert_eq!(fs::read_to_string(librs).unwrap(), "pub fn f() {}");
+Caused by:
+  invalid tarball downloaded, contains an entry at "bar-0.0.1/.cargo-ok" with invalid type Symlink
+
+"#]])
+        .run();
 }
 
 #[cargo_test]